Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement command line parser builtin #663

Open
wants to merge 11 commits into
base: staging
Choose a base branch
from

Conversation

hdwalters
Copy link
Contributor

Fixes #613.

@hdwalters hdwalters requested review from Ph0enixKM and b1ek January 7, 2025 20:07
@hdwalters hdwalters changed the base branch from main to staging January 7, 2025 20:10
@hdwalters hdwalters requested a review from mks-h January 7, 2025 20:11
@@ -70,6 +71,7 @@ struct RunCommand {

/// Arguments passed to Amber script
#[arg(trailing_var_arg = true)]
#[arg(allow_hyphen_values = true)]
Copy link
Contributor Author

@hdwalters hdwalters Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows long options --xyz and short options -x to be passed to the Amber script, without being rejected as invalid by the clap library; but clap still consumes the --help option with amber script.ab --help or ./script.ab --help. I would like to make it pass all options following a positional argument to the Amber script, but I'm not sure how. All suggestions appreciated!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is of course possible to force options to be passed to the script with amber script.ab -- --help. I've tried moving this Clap argument to the end of the struct and adding #[arg(last = true)], but that makes it necessary to call amber script.ab -- arg1 arg2 for all arguments, not just --help.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hdwalters does allow_external_subcommands solve this issue?

Copy link
Contributor Author

@hdwalters hdwalters Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it would. According to the docs, it handles "unexpected positional arguments" (such as foo) not optional ones (such as --help).

I will try it, however.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have to put up with ./script.ab -- --help.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I think that this solution of adding the -- is good enough. It adds the separation between the Amber's arguments and the script args.


impl ParamKind {
fn from(option: String) -> Option<Self> {
let regex = regex!(r"^(?:(\w+)|-(\w)|--(\w+))$");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regular expression is only compiled once; see definition of the regex macro, which uses the once_cell crate to do cached lazy evaluation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be worth supporting long options with hyphens in the name, like --no-foo. If so, will need to modify the regular expression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now supports --no-foo, but not ---no-foo, --no--foo or --no-foo-.

}

impl ParamCli {
pub fn get_payload(&self) -> Option<Payload> {
Copy link
Contributor Author

@hdwalters hdwalters Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR, we introduce the new concept of a "payload", which can be attached to a variable declaration. This allows us to store reference counted CLI parameter data in this struct (where it provides type information for variable getters) and in the parser struct (where it provides parameter information for the help text and command line parsing code).

}

impl Typed for ParamCli {
fn get_type(&self) -> Type {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expose the type of the default value, e.g. Text, [Text], Num, Bool etc, to variable getters, so we can get the values of parameter objects in the same way as ordinary variables.

Some(parser) => parser,
None => return error!(meta, parser_tok, "Expected parser object"),
};
let option = match option.get_literal_text() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The option text, e.g. -x|--xyz, and help text need to be known at compile time, so they can be baked into the case statement and help information in the translated parser code.

src/modules/builtin/cli/parser.rs Outdated Show resolved Hide resolved
}
}

fn create_run_name(meta: &TranslateMetadata, args: &Expr) -> String {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returns the compile time Amber script name if run with amber run or shebang, or the run time Bash script name if built with amber build.

@@ -11,6 +11,16 @@ pub struct Text {
interps: Vec<Expr>,
}

impl Text {
pub fn get_literal_text(&self) -> Option<String> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returns the literal text if compiled from "literal text" and not "interpolated {foo} text".

self.is_const,
);
if let Some(mut payload) = self.expr.get_payload() {
payload.set_var_name(&self.name, self.global_id);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter payloads require the original variable name, so it can be baked into the translated parser code.

.join("\n")
}

fn trim_comment(line: &str) -> &str {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be a bit cleverer about trimming expected output text, because the CLI parser validity tests need to check for CLI help text with leading whitespace.

@hdwalters hdwalters self-assigned this Jan 7, 2025
Copy link
Member

@b1ek b1ek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like how this is implemented, but i'd like to talk about the feature itself in the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] Implement command line parser builtin
3 participants